import { buildSchema, GraphQLSchema, parse, validate, ValidationRule } from 'graphql';
import { Maybe } from 'graphql/jsutils/Maybe';
import { validateSDL } from 'graphql/validation/validate';
import { SDLValidationRule } from 'graphql/validation/ValidationContext';

/**
 * Checks whether the value is an object.
 *
 * `import { isObjectLike } from 'graphql/jsutils/isObjectLike'` does not always have TS files.
 */
function isObjectLike(val: unknown): val is Record<string, unknown> {
  return !!val && typeof val === 'object';
}

/**
 * Creates an object map with the same keys as `map` and values generated by
 * running each value of `map` thru `fn`.
 */
function mapValue<T, V>(
  map: Record<string, T>,
  fn: (value: T, key: string) => V,
): Record<string, V> {
  const result = Object.create(null);

  for (const [key, value] of Object.entries(map)) {
    result[key] = fn(value, key);
  }
  return result;
}

/**
 * Deeply transforms an arbitrary value to a JSON-safe value by calling toJSON
 * on any nested value which defines it.
 */
function toJSONDeep(value: unknown): unknown {
  if (!isObjectLike(value)) {
    return value;
  }

  if (typeof value['toJSON'] === 'function') {
    return value['toJSON']();
  }

  if (Array.isArray(value)) {
    return value.map(toJSONDeep);
  }

  return mapValue(value, toJSONDeep);
}

export function expectJSON(actual: unknown) {
  const actualJSON = toJSONDeep(actual);

  return {
    toDeepEqual(expected: unknown) {
      const expectedJSON = toJSONDeep(expected) as Record<string, unknown>;
      expect(actualJSON).toMatchObject(expectedJSON);
    },
    toDeepNestedProperty(path: string, expected: unknown) {
      const expectedJSON = toJSONDeep(expected);
      expect(actualJSON).toHaveProperty(path, expectedJSON);
    },
  };
}

export function expectToThrowJSON(fn: () => unknown) {
  function mapException(): unknown {
    try {
      return fn();
    } catch (error) {
      return error;
    }
  }

  return expect(mapException());
}

export const testSchema: GraphQLSchema = buildSchema(`
  interface Mammal {
    mother: Mammal
    father: Mammal
  }

  interface Pet {
    name(surname: Boolean): String
  }

  interface Canine implements Mammal {
    name(surname: Boolean): String
    mother: Canine
    father: Canine
  }

  enum DogCommand {
    SIT
    HEEL
    DOWN
  }

  type Dog implements Pet & Mammal & Canine {
    name(surname: Boolean): String
    nickname: String
    barkVolume: Int
    barks: Boolean
    doesKnowCommand(dogCommand: DogCommand): Boolean
    isHouseTrained(atOtherHomes: Boolean = true): Boolean
    isAtLocation(x: Int, y: Int): Boolean
    mother: Dog
    father: Dog
  }

  type Cat implements Pet {
    name(surname: Boolean): String
    nickname: String
    meows: Boolean
    meowsVolume: Int
    furColor: FurColor
  }

  union CatOrDog = Cat | Dog

  type Human {
    name(surname: Boolean): String
    pets: [Pet]
    relatives: [Human]!
  }

  enum FurColor {
    BROWN
    BLACK
    TAN
    SPOTTED
    NO_FUR
    UNKNOWN
  }

  input ComplexInput {
    requiredField: Boolean!
    nonNullField: Boolean! = false
    intField: Int
    stringField: String
    booleanField: Boolean
    stringListField: [String]
  }

  type ComplicatedArgs {
    # TODO List
    # TODO Coercion
    # TODO NotNulls
    intArgField(intArg: Int): String
    nonNullIntArgField(nonNullIntArg: Int!): String
    stringArgField(stringArg: String): String
    booleanArgField(booleanArg: Boolean): String
    enumArgField(enumArg: FurColor): String
    floatArgField(floatArg: Float): String
    idArgField(idArg: ID): String
    stringListArgField(stringListArg: [String]): String
    stringListNonNullArgField(stringListNonNullArg: [String!]): String
    complexArgField(complexArg: ComplexInput): String
    multipleReqs(req1: Int!, req2: Int!): String
    nonNullFieldWithDefault(arg: Int! = 0): String
    multipleOpts(opt1: Int = 0, opt2: Int = 0): String
    multipleOptAndReq(req1: Int!, req2: Int!, opt1: Int = 0, opt2: Int = 0): String
  }

  type QueryRoot {
    human(id: ID): Human
    dog: Dog
    cat: Cat
    pet: Pet
    catOrDog: CatOrDog
    complicatedArgs: ComplicatedArgs
  }

  schema {
    query: QueryRoot
  }

  directive @onField on FIELD
`);

export function expectValidationErrorsWithSchema(
  schema: GraphQLSchema,
  rule: ValidationRule,
  queryStr: string,
) {
  const doc = parse(queryStr);
  const errors = validate(schema, doc, [rule]);
  return expectJSON(errors);
}

export function expectValidationErrors(rule: ValidationRule, queryStr: string) {
  return expectValidationErrorsWithSchema(testSchema, rule, queryStr);
}

export function expectSDLValidationErrors(
  schema: Maybe<GraphQLSchema>,
  rule: SDLValidationRule,
  sdlStr: string,
) {
  const doc = parse(sdlStr);
  const errors = validateSDL(doc, schema, [rule]);
  return expectJSON(errors);
}
